Alt. Lab 02 - Wstep do klas
Alt. Lab 02 - Wstęp do klas
Proste struktury i funkcje w stylu języka C
Do tej pory tworzone na zajęciach struktury zawierały jedynie pola
będące zmiennymi różnych typów. Jako przykład można potraktować
strukturę Student
:
struct Student {
std::string name;
std::string surname;
std::vector<float> grades;
};
Zakładając, że chcemy wyliczyć średnią ocen należy napisać
odpowiednią funkcję, które wykona tę operację dla danego obiektu
struktury Student
:
float calculate_grade(const Student &student) {
float sum = std::accumulate(student.grades.begin(), student.grades.end(), 0.0f);
return sum / student.grades.size();
}
Takie podejście niesie ze sobą pewne konsekwencje: szczególnie w większych projektach powstaje dużo wolnych (nie należących do żadnej klasy), globalnie dostępnych funkcji o różnych nazwach, które nie są hierarchicznie ułożone.
Proste struktury w stylu języka C++
Podstawowe informacje
Funkcje powiązane z daną strukturą można zadeklarować wewnątrz jej deklaracji. Taka funkcja jest wtedy nazywaną metodą i ma dostęp do wszystkich aktualnych wartości przechowywanych w obiekcie danej struktury.
Zmodyfikowana deklaracja struktury Student
może wyglądać
następująco:
struct Student {
std::string name;
std::string surname;
std::vector<float> grades;
float calculate_grade() {
float sum = std::accumulate(grades.begin(), grades.end(), 0.0f);
return sum / grades.size();
}
};
Tym razem metoda calculate_grade
jest zdefiniowana
wewnątrz struktury Student
i nie przyjmuje żadnych
parametrów. Ma ona jednak dostęp do wartości wszystkich pól obiektu
Student
.
Odwoływanie się do metod zadeklarowanych w strukturach odbywa się na analogicznej zasadzie do odwoływania się do pól obiektu:
{"Some", "Student", {2, 3, 4, 5, 3}}; // This creates object of Student type
Student studentstd::cout << student.calculate_grade() << std::endl; // This calls calculate_grade function and prints the result
🛠🔥 Zadanie 🛠🔥
Dodaj metodę print
wewnątrz struktury
Student
, która wydrukuje imię i nazwisko studenta oraz
wszystkie jego oceny:
Jan Kowalski: 3.0 4.5 5.0 3.5
Weryfikacja poprawności wprowadzanych danych
Napisany program w chwili obecnej nie dokonuje żadnego sprawdzania wprowadzanych danych. Studentowi można przypisywać oceny, które będą dowolnymi liczbami.
Przykładowa metoda (wewnątrz struktury Student
), która
umożliwia dodanie nowej oceny wraz z weryfikacją jej poprawności może
wyglądać następująco:
bool add_grade(float grade) {
if (grade >= 2.0 && grade <= 5.0) {
// The grade is valid; let's add it and return true
.push_back(grade);
gradesreturn true;
}
// The grade is invalid; let's return false
return false;
}
Powyższe rozwiązanie nie rozwiązuje jednak wszystkich problemów. Do
obiektu struktury Student
nadal można dodać ocenę pomijając
wywołanie add_grade
:
;
Student student.grades.push_back(8.0); student
Dodatkowo zmienna typu Student
może być zainicjalizowana
ocenami z błędnego przedziału:
{"Jan", "Kowalski", {5, 10, 15}}; Student student
Korzystanie z tak przygotowanego interfejsu wymaga dużej
samodyscypliny oraz przygotowania dokumentacji informującej osobę mającą
używać takiego kodu o konieczności dodawania ocen tylko z użyciem metody
add_grade
.
Klasy jako alternatywa struktur
Problemy opisane powyżej mogą być rozwiązane przy użyciu klas. Koncepcyjnie klasy przypominają struktury: również posiadają pola i metody. Pozwalają jednak osobie projektującej klasę ograniczyć sposoby, w jaki możliwy będzie dostęp do nich „z zewnątrz“.
🛠🔥 Zadanie 🛠🔥
Zmień deklarację struktury struct Student
na
class Student
. Spróbuj skompilować kod odwołujący się do
pól lub metod klasy Student
.
Istnieją trzy modyfikatory dostępu do pól i metod struktury lub klasy: public, protected i private. Początkowo pominiemy wykorzystanie modyfikatora protected.
- Modyfikator public oznacza, że do pól i metod oznaczonych tym modyfikatorem można odwoływać się z kodu znajdującego się poza strukturą lub klasą. Wszystkie pola i metody struktur są domyślnie publiczne.
- Modyfikator private oznacza, że do pól i metod oznaczonych tym modyfikatorem można odwoływać się tylko z kodu znajdującego się wewnątrz metod struktury lub klasy. Wszystkie pola i metody klas są domyślnie prywatne.
Jedyną różnicą między klasami i strukturami w języku C++ jest domyślny modyfikator dostępu. W praktyce deklaracja następującej struktury:
struct Student {
std::string name;
std::string surname;
};
jest równoznaczna następującej deklaracji klasy:
class Student {
public:
std::string name;
std::string surname;
};
Modyfikator obowiązuje dla wszystkich pól i metod zadeklarowanych pod nim, aż do pojawienia się kolejnego modyfikatora.
Dodatkową korzyścią wynikającą z chronienia pól i umożliwienia do nich dostępu jedynie przez metody publiczne jest fakt, że osoba korzystająca z klasy nie musi przejmować się tym, w jaki sposób przechowywane są informacje wewnątrz klasy. Sposób ten może również ulec zmianie wraz z kolejnymi wersjami klasy - dla użycia klasy ważny jest jedynie jej interfejs, czyli funkcje i pola dostępne dla użytkownika klasy. Z tych względów preferowane jest deklarowanie wszystkich pól jako prywatnych, a od tej pory w instrukcjach będą się pojawiały wyłącznie przykłady wykorzystujące klasy.
🛠🔥 Zadanie 🛠🔥
Dodaj modyfikator public do poprzednio utworzonej klasy
Student
. Od tego momentu program powinien działać tak samo
jak przed zamianą struktury na klasę.
Zmień modyfikator dostępu do pola grades
tak, aby
zapobiec jego bezpośredniej modyfikacji.
Konstruktor i destruktor
Konstruktor i destruktor to specjalne metody, które - jak wskazują
ich nazwy - są wywoływane w momencie odpowiednio tworzenia i usuwania
obiektu w pamięci. Mogą one służyć do inicjalizacji pól, alokacji
pamięci czy jej zwalniania. Konstruktor ma nazwę taką samą jak nazwa
klasy/struktury, a destruktor tę samą nazwę poprzedzoną tyldą
(~
). Domyślnie tworzone są pusty domyślny konstruktor i
destruktor.
Konstruktor może mieć dodatkowo argumenty, którymi można na przykład zainicjalizować wartości:
class Student {
public:
(std::string n) {
Student= n;
name }
/* ... */
}
Zmienną można w tym momencie stworzyć w następujący sposób:
("Jan"); Student s1
🛠🔥 Zadanie 🛠🔥
Dodaj do swojej klasy Student
konstruktor, który
umożliwi stworzenie zmiennej typu Student
i jednoczesne
przypisanie mu imienia i nazwiska.
Co się stanie, kiedy spróbujesz utworzyć obiekt nie podając wartości parametrów konstruktora?
; Student s1
Napraw ten problem dodając domyślną wartość argumentów do konstruktora.
Settery, gettery, nazwy pól i metod
W wielu przypadkach stworzona przez nas klasa będzie miała właściwość
(pole), do którego chcemy umożliwić dostęp zarówno do modyfikacji, jak i
odczytu. W tym przypadku konieczne będzie stworzenie pary metod,
nazywanych często odpowiednio setterem i getterem.
Warto przyjąć konwencję nazw, która pozwoli w czytelny sposób
zasugerować do czego służy dana metoda i do których pól się odwołuje,
jednocześnie unikając kolizji nazw pól, metod i argumentów do metod.
Często można spotkać się z dodawaniem do nazw pól prefixu
m_
bądź suffixu _
. Nie ma narzuconego
standardu, ważne jednak, aby w obrębie własnego kodu trzymać się jednej
konwencji. Poniżej przedstawiono prosty przykład klasy z właściwością
index
i najprostszą parą settera i
gettera.
class Student {
public:
void set_index(int index) { // setter
index_ = index;
}
int index() { // getter
return index_;
}
private:
int index_;
};
Zadania końcowe 🛠🔥
1. Student
Rozbuduj klasę Student
, uwzględniając daną
funkcjonalność:
- przechowywanie danych osobowych (imię, nazwisko, numer indeksu)
- przechowywanie zbioru ocen
Publiczny interfejs powinien obejmować:
- ustawienie imienia i nazwiska
- ustawienie indeksu
- wyświetlenie podsumowania informacji o studencie (wraz z ocenami)
- dodawanie oceny
- wyznaczenie średniej
- określenie czy student zdał (maksymalnie 1 ocena 2.0)
Pamiętaj, że oceny mogą przyjąć tylko określone wartości, a dopuszczalne numery indeksów mają od 5 do 6 cyfr. Uniemożliw wpisanie niepoprawnych wartości. Wszystkie pola oznacz jako prywatne.
2. Liczby zespolone
Zaprojektuj klasę Complex
, która przechowa liczbę
zespoloną. Powinna ona mieć konstruktor, który pozwoli na
zainicjalizowanie wartości liczby.
Dodaj do niej metody, które pozwolą na:
- odczyt i modyfikację części rzeczywistej oraz urojonej (niezależnie)
- wyświetlenie liczby w czytelnej postaci
- dodanie do jednej liczby drugiej liczby zespolonej oraz rzeczywistej
Poprawnie zaprojektowana klasa powinna pozwolić na uruchomienie poniższego kodu:
(1.0, -2.0); // creates 1-2i
Complex a(3.14); // creates 3.14
Complex b
.set_im(-5);
b
= a.add(b);
Complex c
.print(); // prints 4.14-7i c
Autorzy: Dominik Pieczyński, Jakub Tomczyński